/*
 * Wood.java
 *
 * Created on Dec 21, 2011
 * 
 * The method getIntesection() is based on Intersection.getIntersection() from
 * the project OpenJGraph.
 *
 * Copyright (c) 2011 Artur Rataj.
 *
 * This code is distributed under the terms of the GNU Library
 * General Public License, either version 3 of the license or, at
 * your option, any later version.
 */

package pl.gliwice.iitis.spatialnn.network.construct;

import java.awt.geom.Line2D;
import java.util.*;

import pl.gliwice.iitis.graphutils.Point;
import pl.gliwice.iitis.graphutils.Rect;
import pl.gliwice.iitis.spatialnn.network.*;

/**
 * Constructs a NN in the form of a set of trees with interconnections. The trees stem from
 * the output membrane, and reach the input membranes.
 * 
 * @author Artur Rataj
 */
public class Wood {
    /**
     * A plain turn similar to the other layers, no multiple layer clouds.
     */
    final boolean DIRECT_TURN = true;

    /**
     * Creates a new instance of <code>Wood</code>.
     */
    public Wood() {
    }
    /**
     * Returns primary locations of inputs of the output membrane.
     * 
     * @param network a network, to which belongs the output membrane
     * @param stats statistic parameters of the constructed network
     * @param forwardAngle mean azimuth, at which proceeds the construction
     * @return candidates for membrane inputs
     */
    public List<InputCandidate> membraneInputsC(Network network, NetworkStats stats,
            double forwardAngle) {
        List<InputCandidate> inputs = new ArrayList<>();
        Membrane out = network.getOutMembrane();
        ConstructionParams cp = stats.membrane.cp;
        int inNum = cp.drawInNum(0, -1);
        for(int i = 0; i < inNum; ++i) {
            double rate = i*1.0/(inNum - 1);
            Point base = new Point(out.locBeg, out.locEnd, rate);
            Point loc = cp.drawInLoc(base, forwardAngle, InputType.ANY,
                    Double.NaN);
            double expression = out.expressionBeg +
                    rate*(out.expressionEnd - out.expressionBeg);
            inputs.add(new InputCandidate(
                    new MembraneTarget(out.key, expression),
                    loc, InputType.ANY));
        }
        return inputs;
    }
    /**
     * Returns primary locations of inputs of a neuron.
     * 
     * @param neuron a neuron, whose inputs are computed
     * @param stats statistic parameters of the constructed layer
     * @param forwardAngle mean azimuth, at which proceeds the construction
     * @param customInputSpread diffference to the forward angle of
     * left/right inputs, <code>Double.NaN</code> for the default of
     * Pi/4
     * @return candidates for inputs
     */
    public List<InputCandidate> neuronInputsC(Neuron neuron, LayerStats stats,
            double forwardAngle, double customInputSpread) {
        List<InputCandidate> inputs = new ArrayList<>();
        ConstructionParams cp = stats.cp;
        NeuronType type = neuron.type;
        int inNum = cp.drawInNum(type.getMinInNum(), type.getMaxInNum());
        if(neuron.type == NeuronType.ADD && inNum != 2)
            neuron = neuron;
        double inAngleSigma = cp.inAngleSigma;
        for(int i = 0; i < inNum; ++i) {
            AbstractTarget target = new NeuronTarget(neuron.key);
            InputType inType;
            if(inAngleSigma >= 0.0)
                inType = InputType.ANY;
            else if(i == 0)
                inType = InputType.LEFT;
            else if(i == 1)
                inType = InputType.RIGHT;
            else
                throw new RuntimeException("invalid input number");
            Point loc = cp.drawInLoc(neuron.loc, forwardAngle, inType,
                    customInputSpread);
            inputs.add(new InputCandidate(target, loc, inType));
        }
        if(neuron.type == NeuronType.ADD && inputs.size() != 2)
            neuron = neuron;
        return inputs;
    }
    /**
     * Creates neurons in a new layer. Layers are constructed since te output
     * one.
     * 
     * @param network network
     * @param stats network statistics
     * @param inputs candidates for inputs from the output membrane and from the
     * already created layers; this method modifies some of the coordinates to these of
     * the final inputs
     * @param layerNum number of the layer to create
     * @param forwardAngle forward angle of inputs of this layer
     * @return a list of neurons in the new layer
     */
    protected List<Neuron> createNeurons(Network network,
            NetworkStats stats, List<InputCandidate>[] inputsC,
            int layerNum, double forwardAngle) {
        LayerStats layerStats = stats.layers.get(layerNum);
        int outputLayerNum = layerStats.outputLayerNum;
        List<InputCandidate> currInputsC = inputsC[outputLayerNum + 1];
        // a distance, at or below which two inputs merge
        double mergeDist;
        if(outputLayerNum == -1)
            mergeDist = stats.membrane.cp.minClusterDist;
        else
            mergeDist = stats.layers.get(outputLayerNum).cp.minClusterDist;
        int num = currInputsC.size();
        List<Neuron> newNeurons = new LinkedList<>();
        Set<Integer> attached = new HashSet<>();
        for(int i = 0; i < num; ++i)
            if(!attached.contains(i)) {
                InputCandidate iP = currInputsC.get(i);
                List<InputCandidate> merges = new LinkedList<>();
                List<Integer> mergesI = new LinkedList<>();
                merges.add(iP);
                mergesI.add(i);
                for(int j = i + 1; j < num; ++j)
                    if(!attached.contains(j)) {
                        InputCandidate jP = currInputsC.get(j);
                        boolean commonComponent = false;
                        for(InputCandidate p : merges)
                            // if no target known, do no merge
                            if(p.target == null ||
                                    p.target.componentsEqual(jP.target)) {
                                commonComponent = true;
                                break;
                            }
                        if(!commonComponent) {
                            double dist = iP.loc.getDist(jP.loc);
                            if(dist <= mergeDist) {
                                merges.add(jP);
                                mergesI.add(j);
                                attached.add(j);
                            }
                        }
                    }
                if(merges.size() > 1) {
                    double x = 0.0;
                    double y = 0.0;
                    for(InputCandidate p : merges) {
                        x += p.loc.x;
                        y += p.loc.y;
                    }
                    x = x/merges.size();
                    y = y/merges.size();
                    for(InputCandidate p : merges)
                        p.loc.set(x, y);
                }
                Neuron n = new Neuron(network, network.neurons.size(),
                        layerStats.type, layerNum, new Point(iP.loc),
                        forwardAngle);
                if(n.layerNum == 1 && false)
                    System.out.println(n.toString());
                newNeurons.add(n);
                network.add(n);
                AbstractTarget nT = new NeuronTarget(n.key);
                for(int j : mergesI) {
                    AbstractTarget target = currInputsC.get(j).target;
                    n.addOutputComponent(target);
                    if(target != null) {
                        Component c = network.getComponent(target);
                        c.addInput(nT);
                    }
                }
            }
        return newNeurons;
    }
    /**
     * Returns the intersection point of two segments.
     *
     * @param line1 First line
     * @param line2 Second line
     * @return The Point object where the two lines intersect. This method
     * returns null if the two lines do not intersect.
     * @throws <tt>MultipleIntersectionException</tt> when the two lines have
     * more than one intersection point.
     */
    static public Point getIntersection(Line2D.Double line1, Line2D.Double line2) {
        double dyline1, dxline1;
        double dyline2, dxline2, e, f;
        double x1line1, y1line1, x2line1, y2line1;
        double x1line2, y1line2, x2line2, y2line2;

        if (!line1.intersectsLine(line2)) {
            return null;
        }

        /*
         * first, check to see if the segments intersect by parameterization on
         * s and t; if s and t are both between [0,1], then the segments
         * intersect
         */
        x1line1 = line1.getX1();
        y1line1 = line1.getY1();
        x2line1 = line1.getX2();
        y2line1 = line1.getY2();

        x1line2 = line2.getX1();
        y1line2 = line2.getY1();
        x2line2 = line2.getX2();
        y2line2 = line2.getY2();

        /*
         * check to see if the segments have any endpoints in common. If they
         * do, then return the endpoints as the intersection point
         */
        if ((x1line1 == x1line2) && (y1line1 == y1line2)) {
            return (new Point(x1line1, y1line1));
        }
        if ((x1line1 == x2line2) && (y1line1 == y2line2)) {
            return (new Point(x1line1, y1line1));
        }
        if ((x2line1 == x1line2) && (y2line1 == y1line2)) {
            return (new Point(x2line1, y2line1));
        }
        if ((x2line1 == x2line2) && (y2line1 == y2line2)) {
            return (new Point(x2line1, y2line1));
        }

        dyline1 = -(y2line1 - y1line1);
        dxline1 = x2line1 - x1line1;

        dyline2 = -(y2line2 - y1line2);
        dxline2 = x2line2 - x1line2;

        e = -(dyline1 * x1line1) - (dxline1 * y1line1);
        f = -(dyline2 * x1line2) - (dxline2 * y1line2);

        /*
         * compute the intersection point using ax+by+e = 0 and cx+dy+f = 0
         *
         * If there is more than 1 intersection point between two lines,
         */
        if ((dyline1 * dxline2 - dyline2 * dxline1) == 0) {
            // throw new MultipleIntersectionException();
            return null;
        }
        return (new Point(
                -(e * dxline2 - dxline1 * f) / (dyline1 * dxline2 - dyline2 * dxline1),
                -(dyline1 * f - dyline2 * e) / (dyline1 * dxline2 - dyline2 * dxline1)));
    }
    /**
     * Extends an input candidate so that it intersects an input membrane, if possible.
     * The input candidate must output to a neuron.
     * 
     * @param network network
     * @param in input candidate to extend by modifying its location
     * @param membrane membrane to extend the input to, does not need to belong to
     * the network; null for any input membrane in the network
     * @return if extended, the target of the membrane which intersects the input candidate;
     * otherwise null
     */
    public MembraneTarget extendIn(Network network, InputCandidate in,
            Membrane membrane) {
        Neuron n = network.neuronMap.get(((NeuronTarget)in.target).key);
        double extX = n.loc.x + 1000.0*(in.loc.x - n.loc.x);
        double extY = n.loc.y + 1000.0*(in.loc.y - n.loc.y);
        Line2D.Double inLine = new Line2D.Double(n.loc.x, n.loc.y,
                extX, extY);
        MembraneTarget found = null;
        for(Membrane m : network.membranes) {
            if(membrane != null)
                m = membrane;
            if(membrane != null || m.type == MembraneType.IN) {
                Line2D.Double mLine = new Line2D.Double(m.locBeg.x, m.locBeg.y,
                        m.locEnd.x, m.locEnd.y);
                Point i = getIntersection(inLine, mLine);
                if(i != null) {
                    extX = i.x;
                    extY = i.y;
                    double begDist = new Point(extX, extY).getDist(m.locBeg);
                    double outDist = new Point(m.locEnd).getDist(m.locBeg);
                    double ratio = begDist/outDist;
                    double expression = m.expressionBeg + ratio*(m.expressionEnd - m.expressionBeg);
                    found = new MembraneTarget(m.key, expression);
                    break;
                }
            }
            if(membrane != null)
                break;
        }
        if(found != null) {
            in.loc.x = extX;
            in.loc.y = extY;
        }
        return found;
    }
    /*
     * An utility method of <code>prune</code> -- returns true if a list
     * contains no non--null elements.
     * 
     * @param output a list
     * @return if no non--null elements found in the list
     */
    /*
    private boolean noOutputs(AbstractTarget[] output) {
        boolean found = false;
        for(AbstractTarget t : output)
            if(t != null) {
                found = true;
                break;
            }
        return found;
    }*/
    /**
     * Prunes this network form all invalid neurons.<br>
     * 
     * Currently, it checks if (1) a neuron has exactly two outputs if
     * <code>ConstructionParams.inAngleSigma</code> is negative,
     * (2) a neuron has at least a single input and (3) a single output,
     * and then recursively prunes invalid neurons.<br>
     * 
     * @param network network to prune
     * @param stats statistics of the network
     * @param layers layers, from which remove invalid neurons;
     * null for all layers
     */
    void prune(Network network, NetworkStats stats, Set<Integer> layers) {
        boolean stable;
        // Neuron nn = network.neuronMap.get(50);
        List<Neuron> oldList = network.neurons;
        do {
            stable = true;
            List<Neuron> newList = new LinkedList<>();
            for(Neuron n : oldList) {
                boolean outside = layers != null &&
                        !layers.contains(n.layerNum);
                ConstructionParams cp = stats.layers.get(n.layerNum).cp;
                if(!outside && (
                        (cp.inAngleSigma < 0.0 &&
                        n.inputs.length != 2) ||
                        (n.type == NeuronType.ABS &&
                        n.inputs.length < 2) ||
                        n.inputs.length == 0 ||
                        n.output.length == 0)) {
                    // remove
                    if(n.key == 29 || n.key == 138)
                        n = n;
                    for(AbstractTarget t : n.inputs) {
                        Component c = network.getComponent(t);
                        boolean found = false;
                        for(int i = 0; i < c.getOutputComponents().length; ++i) {
                            NeuronTarget nU = (NeuronTarget)c.getOutputComponents()[i];
                            if(nU.key == n.key) {
                                c.removeOutputComponent(i);
                                found = true;
                                break;
                            }
                        }
                        if(!found)
                            throw new RuntimeException("output component not found");
                    }
                    for(AbstractTarget t : n.output) {
                        Component c = network.getComponent(t);
                        boolean found = false;
                        for(int i = 0; i < c.getInputs().length; ++i) {
                            NeuronTarget nU = (NeuronTarget)c.getInputs()[i];
                            if(nU.key == n.key) {
                                c.removeInput(i);
                                found = true;
                                break;
                            }
                        }
                        if(!found)
                            throw new RuntimeException("input not found");
                    }
                    stable = false;
                } else
                    newList.add(n);
            }
            oldList = newList;
        } while(!stable);
        network.neurons.clear();
        network.neuronMap.clear();
        for(Neuron n : oldList)
            network.add(n);
    }
    /**
     * Sets membrane edges to ractangle corners. The rectangle's height must be zero.
     * Then, shrinks the membrane's length by a given scale, without transposing the
     * membrane's middle.
     * 
     * @param m membrane to modify
     * @param r rectangle
     * @param s scale of the membrane
     */
    protected void shrink(Membrane m, Rect r, double s) {
        m.locBeg = r.min;
        m.locEnd = r.max;
        double dist = m.locEnd.x - m.locBeg.x;
        double scaledDist = dist*s;
        double cuts = (dist - scaledDist)/2.0;
        m.locBeg.x += cuts;
        m.locEnd.x -= cuts;
    }
    /**
     * Positiones input membranes.
     * 
     * @param network network
     * @param topInputs candidates in the top layer
     * @param symmetricInput if the network has symmetric input
     * @param bound bound on the location of top layer neurons
     * @param boundRatio which ratio of neuron's inputs must fit into a membrane
     * @param turnBound bound of the turn zone, null for no turn zone
     */
    protected void positionInMembranes(Network network, List<InputCandidate> topInputs,
            boolean symmetricInput, Rect bound, double boundRatio, Rect turnBound) {
        Membrane leftM;
        Membrane rightM;
        if(symmetricInput) {
            leftM = network.getLeftMembrane();
            rightM = network.getRightMembrane();
        } else {
            leftM = null;
            rightM = null;
        }
        double yTop = bound.max.y;
        double yMid = (bound.min.y + 2*bound.max.y)/3.0;
        yTop = yMid;
        final double LARGE = 1000.0;
        double STEP = 0.002;
        yTop += STEP;
        if(symmetricInput) {
            boolean separate;
            do {
                List<InputCandidate> leftIC = new LinkedList<>();
                List<InputCandidate> rightIC = new LinkedList<>();
                Membrane tM = new Membrane(null, null,
                        new Point(-LARGE, yTop), new Point(LARGE, yTop),
                        0.0, 1.0);
                Set<Component> leftN = new HashSet<>();
                Set<Component> rightN = new HashSet<>();
                for(InputCandidate in : topInputs) {
                    InputCandidate iC = new InputCandidate(in.target, new Point(in.loc),
                        in.type);
                    MembraneTarget mT = extendIn(network, iC, tM);
                    if(mT != null) {
                        switch(in.type) {
                            case LEFT:
                                leftIC.add(iC);
                                leftN.add(network.getComponent(iC.target));
                                break;

                            case RIGHT:
                                rightIC.add(iC);
                                rightN.add(network.getComponent(iC.target));
                                break;

                            default:
                                throw new RuntimeException("top layer lacks symmetric inputs but the network " +
                                        "input membranes are symmetric");
                        }
                    }
                }
                Set<Component> commonN = new HashSet<>();
                for(Component p : leftN)
                    if(rightN.contains(p))
                        commonN.add(p);
                List<InputCandidate> pLeftIC = new LinkedList<>();
                List<InputCandidate> pRightIC = new LinkedList<>();
                for(InputCandidate in : leftIC)
                    if(commonN.contains(network.getComponent(in.target)))
                        pLeftIC.add(in);
                for(InputCandidate in : rightIC)
                    if(commonN.contains(network.getComponent(in.target)))
                        pRightIC.add(in);
                Rect leftB = InputCandidate.getTightenedBound(pLeftIC, boundRatio);
                Rect rightB = InputCandidate.getTightenedBound(pRightIC, boundRatio);
                double overlap = Math.max(0.0, leftB.max.x - rightB.min.x);
                double whole = rightB.max.x - leftB.min.x;
                separate = overlap/whole < 0.4 || true;
                if(separate) {
                    if(overlap > 0.0 && false) {
                        double divide = (leftB.max.x + rightB.min.x)/2.0;
                        leftB.max.x = divide - STEP;
                        rightB.min.x = divide + STEP;
                    }
                    shrink(leftM, leftB, 1.0);
                    shrink(rightM, rightB, 1.0);
                    // InputCandidate.getTightenedBound(pRightIC, boundRatio);
                } else {
                    yTop += STEP;
                    STEP *= 1.2;
                }
            } while(!separate);
        } else {
            List<InputCandidate> commonIC = new LinkedList<>();
            Membrane tM = new Membrane(null, null,
                    new Point(-LARGE, yTop), new Point(LARGE, yTop),
                    0.0, 1.0);
            for(InputCandidate in : topInputs) {
                InputCandidate iC = new InputCandidate(in.target, new Point(in.loc),
                    in.type);
                MembraneTarget mT = extendIn(network, iC, tM);
                // System.out.println(yTop + " : " + iC.loc.toString());
                if(mT != null)
                    commonIC.add(iC);
            }
            Rect commonB = InputCandidate.getTightenedBound(commonIC, boundRatio);
            Membrane commonM = network.getInputMembrane();
            commonM.locBeg = commonB.min;
            commonM.locEnd = commonB.max;
            if(turnBound != null && true) {
                double mid = (turnBound.min.x + turnBound.max.x)/2.0;
                commonM.locBeg.x = (turnBound.min.x + 0*mid)/1.0;
                commonM.locEnd.x = (turnBound.max.x + 0*mid)/1.0;
            }
        }
    }
    /**
     * Connects the top layer to the input membranes.
     * 
     * @param network network
     * @param topInputs candidates in the top layer
     * @param symmetricInput if the network has symmetric inputs
     */
    private void connectInMembranes(Network network, List<InputCandidate> topInputs,
            boolean symmetricInput) {
        Membrane leftM;
        Membrane rightM;
        if(symmetricInput) {
            leftM = network.getLeftMembrane();
            rightM = network.getRightMembrane();
        } else {
            leftM = null;
            rightM = null;
        }
        for(InputCandidate in : topInputs) {
            Membrane tM;
            if(symmetricInput) {
                switch(in.type) {
                    case LEFT:
                        tM = leftM;
                        break;
                        
                    case RIGHT:
                        tM = rightM;
                        break;
                        
                    default:
                        throw new RuntimeException("invalid input type for a network with symmetric input");
                }
            } else
                tM = null;
            MembraneTarget mT = extendIn(network, in, tM);
            if(mT != null) {
                Neuron n = network.neuronMap.get(((NeuronTarget)in.target).key);
                if(n.key == 263)
                    n = n;
                boolean sameMembrane = false;
                for(AbstractTarget t : n.getInputs()) {
                    if(mT.componentsEqual(t)) {
                        sameMembrane = true;
                        break;
                    }
                }
                if(!sameMembrane || true) {
                    int index = n.inputs.length;
                    n.addInput(mT);
                    Membrane m = network.membraneMap.get(mT.key);
                    m.addOutputNeuron(new NeuronTarget(n.key));
                }
            }
        }
    }
    /**
     * Scales the output membrane to match the neurons that output to the membrane.
     * 
     * @param network network
     * @param turnZone if there is a turn zone in the network
     */
    protected void scaleOutMembrane(Network network, boolean turnZone) {
        Membrane outMembrane = network.getOutMembrane();
        List<InputCandidate> outIC = new LinkedList<>();
        outMembrane.computeExpressions(network);
        for(double e : outMembrane.expressions) {
            MembraneTarget t = new MembraneTarget(outMembrane.key,
                    e);
            InputCandidate iC = new InputCandidate(null, new Point(t.getLoc(network)),
                null);
            outIC.add(iC);
        }
        Rect outB = InputCandidate.getBound(outIC);
        double prevL;
        double prevR;
        double currL;
        double currR;
        if(turnZone) {
            prevL = outMembrane.locBeg.y;
            prevR = outMembrane.locEnd.y;
            currL = outB.max.y;
            currR = outB.min.y;
        } else {
            prevL = outMembrane.locBeg.x;
            prevR = outMembrane.locEnd.x;
            currL = outB.min.x;
            currR = outB.max.x;
        }
        double ratioL = (currL - prevL)/(prevR - prevL);
        double ratioR = (currR - prevL)/(prevR - prevL);
        double eBeg = outMembrane.expressionBeg;
        double eEnd = outMembrane.expressionEnd;
        if(true) {
            outMembrane.expressionBeg = eBeg + ratioL*(eEnd - eBeg);
            outMembrane.expressionEnd = eBeg + ratioR*(eEnd - eBeg);
            if(turnZone) {
                outMembrane.locBeg.y = currL;
                outMembrane.locEnd.y = currR;
            } else {
                outMembrane.locBeg.x = currL;
                outMembrane.locEnd.x = currR;
            }

        }
    }
    /**
     * Moves input candidates to some existing neurons.
     * 
     * @param network network
     * @param source neurons, to output to other neurons via <code>target</code>
     * @param target input candidates to move to nearest neurons in <code>source</code>
     */
    private void match(Network network, List<Neuron> source, List<InputCandidate> target) {
        // keys: target in the turn layer, sources in the turn zone
        Map<Component, Set<Component>> c = new HashMap<>();
        for(InputCandidate ic : target) {
            Component t = network.getComponent(ic.target);
            if(t instanceof Neuron) {
                Neuron n_ = (Neuron) t;
                if(n_.key == 7)
                    n_ = n_;
            }
            if(c.get(t) == null)
                c.put(t, new HashSet<Component>());
            double minDist = Double.MAX_VALUE;
            Neuron best = null;
            for(Neuron s : source) {
                if(s.key == 181 || s.key == 187)
                    s = s;
                if(!c.get(t).contains(s)) {
                    double dist = s.loc.getDist(ic.loc);
                    if(minDist > dist) {
                        minDist = dist;
                        best = s;
                    }
                }
            }
            if(best == null)
                throw new RuntimeException("cannot connect");
            if(best.key == 29)
                best = best;
            t.addInput(new NeuronTarget(best.key));
            best.addOutputComponent(ic.target);
            c.get(t).add(best);
        }
    }
    /**
     * Vertically scales neurons in a given layer, together with their inputs.
     * 
     * @param network network
     * @param layer layer to scale
     * @param inputs inputs, which should be scaled together with the layer, null for
     * none
     * @param yBottom the scaling's y origin: at this height the objects are not moved
     * @param scale scale
     * @param translateInputs the inputs should not be scaled, but only translated
     * as much, as the neurons to which the inputs target
     */
    protected void scale___Layer(Network network, int layer, List<InputCandidate> inputs,
            double yBottom, double scale, boolean translateInputs) {
        if(inputs != null) {
            for(InputCandidate c : inputs)
                if(translateInputs) {
                    // translate input candidates
                    Neuron n = (Neuron)network.getComponent(c.target);
                    double newPos = scale*(n.loc.y - yBottom) +
                            yBottom;
                    double offset = newPos - n.loc.y;
                    c.loc.y += offset;
                } else
                    // scale input candidates
                    c.loc.y = scale*(c.loc.y - yBottom) +
                            yBottom;
        }
        // scale neurons
        List<Neuron> ns = network.getLayerNeurons(layer);
        for(Neuron n : ns)
            n.loc.y = scale*(n.loc.y - yBottom) +
                    yBottom;
    }
    /**
     * Vertically translates neurons in a given layer, together with their inputs.
     * 
     * @param network network
     * @param layer layer to scale
     * @param inputs inputs, which should be scaled together with the layer, null for
     * none
     * @param offset vertical offset
     */
    protected void translateLayer(Network network, int layer, List<InputCandidate> inputs,
            double offset) {
        if(inputs != null) {
            for(InputCandidate c : inputs)
                // translate input candidates
                c.loc.y += offset;
        }
        // translate neurons
        List<Neuron> ns = network.getLayerNeurons(layer);
        for(Neuron n : ns)
            n.loc.y += offset;
    }
    /**
     * Adds neurons and connections to a network.
     * 
     * @param network a network to be populated, it should initially contain no neurons
     * @param stats statistic parameters of the network to construct
     */
    public void construct(Network network, NetworkStats stats) {
        /**
         * Ratio for computing bounds.
         */
        final double BOUND_RATIO = 0.8 + 0.1*01;
        if(!network.neurons.isEmpty())
            throw new RuntimeException("a network to populate already contains neurons");
        NetworkType type = network.getType();
        double forwardAngle = network.getOutMembrane().getAngle();
        boolean turnZone;
        switch(type) {
            case STRAIGHT:
                turnZone = false;
                break;
                
            case TURNED:
                turnZone = true;
                break;
                
            default:
                throw new RuntimeException("unsupported network type: " +
                        type.toString());
        }
        if(turnZone && DIRECT_TURN && stats.turnSectionNum != 1)
            throw new RuntimeException("direct turn allows only a single turn section");
        Membrane leftM = network.getLeftMembrane();
        Membrane rightM = network.getRightMembrane();
        boolean symmetricInput = leftM != null && rightM != null;
        if(symmetricInput) {
            if(Math.abs(leftM.locBeg.y - leftM.locEnd.y) > 1e-6)
                throw new RuntimeException("input membranes must be horizontal");
            if(Math.abs(leftM.locBeg.y - rightM.locBeg.y) > 1e-6)
                throw new RuntimeException("input membranes must of equal height");
            if(leftM.locBeg.x >= leftM.locEnd.x)
                throw new RuntimeException("left input membrane does not have its beginning on the left");
            if(rightM.locBeg.x >= rightM.locEnd.x)
                throw new RuntimeException("right input membrane does not have its beginning on the left");
        }
        if(turnZone != (stats.turnLayerNum != NetworkStats.TURN_NUM_NONE))
            throw new RuntimeException("network type/stats mismatch: turn zone");
        // contains inputs in the order: output membrane, subsequent layers
        int layersNum = stats.layers.size();
        List<InputCandidate>[] inputs = (List<InputCandidate>[])new List[layersNum + 1];
        // inputs originally generated by the neurons
        List<InputCandidate>[] origInputs = (List<InputCandidate>[])new List[layersNum + 1];
        inputs[0] = origInputs[0] = membraneInputsC(network, stats, forwardAngle);
        List<AbstractTarget> outputs = new ArrayList<>();
        Membrane outMembrane = network.getOutMembrane();
        double turnMeanYBottom = Double.NaN;
        List<Neuron> lastNeurons = null;
        if(stats.turnLayerNum + stats.turnSectionNum*stats.turnSectionSize >
                layersNum - 1)
            throw new RuntimeException("turn layer has no upper end");
        for(int layer = 0; layer < layersNum; ++layer) {
            LayerStats layerStats = stats.layers.get(layer);
            if(layer == 0 && layerStats.outputLayerNum != -1)
                throw new RuntimeException(
                        "the outputmost layer must output to a membrane");
            // if the current layer is the last one in the turn zone
            boolean turnZoneEnd = turnZone && layer == stats.turnLayerNum +
                        stats.turnSectionNum*stats.turnSectionSize;
            boolean turnZoneStart = turnZone && layer == stats.turnLayerNum;
            boolean turnSectionEnd = turnZone && layer > stats.turnLayerNum &&
                    (layer - 1 - stats.turnLayerNum)%
                    stats.turnSectionSize == stats.turnSectionSize - 1;
            List<Neuron> neurons = createNeurons(network, stats,
                    inputs, layer, forwardAngle);
            lastNeurons = neurons;
            double customInputSpread;
            if(turnZone && layer == layersNum - 1 &&
                    layerStats.type.getConstructionParams(false).inAngleSigma < 0.0) {
                // symmetric inputs out of input membranes, spread them according to the turn
                // zone's bound
                /*
                customInputSpread = -(Math.PI/2.0 - Math.atan(3.0*
                        (network.turnBound.max.y - network.turnBound.min.y)/
                        (network.turnBound.max.x - network.turnBound.min.x)));
                        */
                customInputSpread = NeuronType.CUSTOM_INPUT_SPREAD;
                network.symmetricInputSpread = customInputSpread;
            } else {
                customInputSpread = Double.NaN;
                network.symmetricInputSpread = Math.PI/4.0;
            }
            // customInputSpread = Math.PI/12.0;
            List<InputCandidate> l = new ArrayList<>();
            for(Neuron n : neurons) {
                if(n.key == 7)
                    n = n;
                l.addAll(neuronInputsC(n, layerStats, forwardAngle,
                        customInputSpread));
                if(n.type == NeuronType.ADD && l.size() > 2)
                    n = n;
            }
            origInputs[layer + 1] = l;
            // System.out.println("mean(" + (xSum/l.size()) + ":" + (ySum/l.size()) + ")");
            if(turnZoneStart && !DIRECT_TURN) {
                // start the turn zone
                double xSum = 0.0;
                double ySum = 0.0;
                for(InputCandidate loc : l) {
                    xSum += loc.loc.x;
                    ySum += loc.loc.y;
                }
                double inputMeanX = xSum/l.size();
                network.turnBound = InputCandidate.getTightenedBound(l, BOUND_RATIO*0+1);
                network.turnBound.max.x = 2*inputMeanX - network.turnBound.min.x;
                int startInputsNum = (int)Math.round(l.size()*1.0/stats.turnSectionNum /**
                    stats.layers.get(layer).type.getConstructionParams(true).inNum.mean */);
                for(int zone = 0; zone < stats.turnSectionNum; ++zone) {
                    List<InputCandidate> m = new LinkedList<>();
                    double xMin = network.turnBound.min.x;
                    double xMax = network.turnBound.max.x;
                    double sectionHeight = (network.turnBound.max.y - network.turnBound.min.y)/
                            stats.turnSectionNum;
                    //turnMeanYBottom = network.turnBound.min.y +
                    //        (zone + 0.5)*sectionHeight;
                    //Gaussian yScatter = new Gaussian(0, /*stats.layers.get(layer + 1).type.
                    //        getConstructionParams(false).inDist.mean*/
                    //        /*(network.turnBound.max.y - network.turnBound.min.y)/
                    //        stats.turnSectionNum*/
                    //        sectionHeight/2.0/2);
                    for(int i = 0; i < startInputsNum; ++i)
                        // connection to such candidate results in a null output component,
                        // which needs to be removed later
                        m.add(new InputCandidate(null, new Point(
                                xMin + i*(xMax - xMin)/(startInputsNum - 1),
                                network.turnBound.min.y + Math.random()*sectionHeight),
                                null));
                    inputs[layer + 1 + zone*stats.turnSectionSize] = m;
                }
                forwardAngle += Math.PI/2.0;
            } else if(DIRECT_TURN || (!turnSectionEnd ||
                    // candidates of the last layer in the turn zone are not replaced
                    turnZoneEnd))
                inputs[layer + 1] = l;
            if(DIRECT_TURN && turnZoneStart)
                forwardAngle += Math.PI/2.0;
            if(turnZoneEnd && !DIRECT_TURN) {
                // scale the turn sections
                for(int zone = stats.turnSectionNum - 1; zone >= 0; --zone) {
                    // this section's bottom layer
                    int b = stats.turnLayerNum + 1 + zone*stats.turnSectionSize;
                    // this section's top layer
                    int i = stats.turnLayerNum + 1 + zone*stats.turnSectionSize +
                            (stats.turnSectionSize - 1);
                    double yTopFixed;
                    boolean finalZone = zone == stats.turnSectionNum - 1;
                    if(!finalZone) {
                        // this zone connect to a subsequent turn section
                        //
                        // next section's layer, that outputs to this section
                        int j = i + 1 + stats.turnSectionSkip;
                        List<Neuron> fixed = network.getLayerNeurons(j);
                        yTopFixed = 0.0;
                        for(Neuron n : fixed)
                            yTopFixed += n.loc.y;
                        yTopFixed = yTopFixed/fixed.size();
                    } else {
                        // this zone connects to outside of the turn section
                        yTopFixed = network.turnBound.max.y;
                    }
                    double yTopScalable = 0.0;
                    List<InputCandidate> scalable = origInputs[i + 1];
                    for(InputCandidate ic : scalable)
                        yTopScalable += ic.loc.y;
                    yTopScalable = yTopScalable/scalable.size();
                    double yBottom = 0.0;
                    List<Neuron> bottom = network.getLayerNeurons(b);
                    for(Neuron n : bottom)
                        yBottom += n.loc.y;
                    yBottom = yBottom/bottom.size();
                    double scale = (yTopFixed - yBottom)/(yTopScalable - yBottom);
                    double offset = yTopFixed - yTopScalable;
                    scale = 1.0;
                    // offset = 0.0;
                    for(int q = b + 1; q <= i; ++q)
                        // scaleLayer(network, q, origInputs[q + 1], yBottom, scale,
                        //         finalZone);
                        translateLayer(network, q, origInputs[q + 1], offset);
                }
                // make the turn inputs
                /*
                // first, prune invalid neurons in the turn zone
                Set<Integer> turnZoneLayers = new HashSet<>();
                for(int i = stats.turnLayerNum + 1; i <= layer; ++i)
                    turnZoneLayers.add(i);
                prune(network, stats, turnZoneLayers);
                */
                // match the turn layer to begins of turn zones
                List<Neuron> source = new LinkedList<>();
                for(int i = stats.turnLayerNum + 1; i <= layer; i += stats.turnSectionSize)
                    source.addAll(network.getLayerNeurons(i));
                for(Neuron s : source)
                    // remove the temporary null outputs
                    s.removeOutputComponent(0);
                match(network, source, origInputs[stats.turnLayerNum + 1]);
                // match the ends of turn zones to next zones
                for(int zone = 0; zone < stats.turnSectionNum - 1; ++zone) {
                    int i = stats.turnLayerNum + 1 + zone*stats.turnSectionSize +
                            (stats.turnSectionSize - 1);
                    int j = i + 1 + stats.turnSectionSkip;
                    source.clear();
                    source.addAll(network.getLayerNeurons(j));
                    match(network, source, origInputs[i + 1]);
                }
            }
        }
        // the top layer
        List<InputCandidate> topInputs = inputs[stats.layers.size()];
        // top bound on the top layer neurons
        /*
        Rect topBound = InputCandidate.getTightenedBound(
                inputs[stats.layers.size()], 0.5*0+1);
         */
        List<InputCandidate> ti = new LinkedList<>();
        for(Neuron n : lastNeurons)
            ti.add(new InputCandidate(null, n.loc, null));
        Rect topBound = InputCandidate.getTightenedBound(ti, 0.8);
        positionInMembranes(network, topInputs, symmetricInput,
                topBound, BOUND_RATIO, network.turnBound);
        connectInMembranes(network, topInputs, symmetricInput);
        prune(network, stats, null);
        scaleOutMembrane(network, turnZone);
        if(!pl.gliwice.iitis.spatialnn.Main.TIME_MEASURE) {
            network.checkConnectivity();
            pack(network);
        }
    }
    /**
     * Corrects minimum and maximum values in respect to a point.
     * 
     * @param min minimum value
     * @param max maximum value
     * @param p point
     */
    protected void correct(Point min, Point max, Point p) {
        if(min.x > p.x)
            min.x = p.x;
        if(min.y > p.y)
            min.y = p.y;
        if(max.x < p.x)
            max.x = p.x;
        if(max.y < p.y)
            max.y = p.y;
    }
    /**
     * Moves the components and resizes the network, so that the components fit
     * into the network's region.
     * 
     * @param network network to pack
     */
    protected void pack(Network network) {
        final Point MARGIN = new Point(0.05, 0.05);
        // find min.max
        Point min = new Point(Double.MAX_VALUE, Double.MAX_VALUE);
        Point max = new Point(-Double.MAX_VALUE, -Double.MAX_VALUE);
        for(Membrane m : network.membranes) {
            correct(min, max, m.locBeg);
            correct(min, max, m.locEnd);
        }
        for(Neuron n : network.neurons)
            correct(min, max, n.loc);
        // disable drawing of the turn bound
        network.turnBound = null;
        if(network.turnBound != null) {
            correct(min, max, network.turnBound.min);
            correct(min, max, network.turnBound.max);
        }
        min.subtract(MARGIN);
        // move components
        for(Membrane m : network.membranes) {
            m.locBeg.subtract(min);
            m.locEnd.subtract(min);
        }
        if(network.turnBound != null) {
            network.turnBound.min.subtract(min);
            network.turnBound.max.subtract(min);
        }
        for(Neuron n : network.neurons)
            n.loc.subtract(min);
        // resize
        max.subtract(min);
        max.add(MARGIN);
        network.size.set(max);
    }
}
